//    Copyright (C) Kherty.  All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Reflection;
using OpenLS.Spreadsheet.Expressions;
using OpenLS.Spreadsheet.Extensibility;

namespace OpenLS.Spreadsheet.Functions
{
    ///<summary>
    /// Represents a function defined in an add in
    ///</summary>
    public class AddInFunction : Function
    {
        private readonly IFunctionLocalizer _localizer;
        private readonly bool _noValueAccess;

        internal readonly MethodInfo Method;

        private ParametersConverter _parameterConverter;
        private int? _parameterCountCache;

        internal AddInFunction(MethodInfo method, IFunctionLocalizer loc, AddIn addin)
        {
            this._addIn = addin;
            Method = method;
            _name = Method.Name.ToUpper();
            foreach (AddInFunctionAttribute o in Method.GetCustomAttributes(typeof (AddInFunctionAttribute), false))
            {
                _name = o.Name.ToUpper();
            }
            _localizer = loc ?? new CanonicalLocalizer();
            _noValueAccess = method.GetCustomAttributes(typeof (NoValueAccessAttribute), true).Length != 0;
            //\\Debug.Assert(Method.Name.ToUpper() != "IF");
        }

        private readonly string _name;
        private readonly AddIn _addIn;

        ///<summary>
        /// Gets the name of the function. This name is not localized
        ///</summary>
        public override string Name
        {
            get { return _name; }
        }

        internal override bool UsesVar
        {
            get {
                bool result =  Enumerable.Any(Method.GetParameters(),
                                      p => p.GetCustomAttributes(typeof (OptionalAttribute), false).Length > 0); 
                if (!result)
                {
                    var methodParameters = Method.GetParameters();
                    if (methodParameters.Length > 0)
                    {
                        var parameterInfo = methodParameters[methodParameters.Length - 1];
                        if (typeof(IEnumerable).IsAssignableFrom(parameterInfo.ParameterType) && parameterInfo.ParameterType != typeof(string))
                            result = true;
                    }
                }
                Contract.Assert(Name != "SEARCH" || result);
                return result;
            }

        }

        ///<summary>
        /// Get the localized name of the function
        ///</summary>
        public override string LocalizedName
        {
            get { return _localizer.GetLocalName(Name); }
        }

        internal override bool NoValueAccess
        {
            get { return _noValueAccess; }
        }

        internal override int MaximumParameterCount
        {
            get
            {
                if (_parameterCountCache == null)
                {
                    int res = 0;
                    foreach (ParameterInfo info in Method.GetParameters())
                    {
#if DEBUG
                        var test = info.GetCustomAttributes(typeof (WorksheetContextAttribute), false);
                        test = test;
#endif
                        if (info.GetCustomAttributes(typeof (WorksheetContextAttribute), false).Length > 0)
                        {
                            continue;
                        }
                        if (info.GetCustomAttributes(typeof (CellContextAttribute), false).Length > 0)
                            continue;
                        res++;
                    }
                    _parameterCountCache = res;
                }
                return _parameterCountCache.Value;
            }
        }

        private ParametersConverter ParameterConverter
        {
            get
            {
                if (_parameterConverter == null)
                    _parameterConverter = new ParametersConverter(Method);
                return _parameterConverter;
            }
        }

        internal AddIn AddIn
        {
            get { return _addIn; }
        }

        protected override object InvokeImp(Cell caller, Worksheet worksheet, Workbook workbook, object[] args)
        {
            Error error = null;
#if DEBUGNOTUSED
            if (Method.Name == "VLookup" && args.Length == 4 && args[2] is Cell)
            {
                Cell cell = (Cell)args[2];
                if (cell.Label == "T8")
                    Debugger.Break();
            }
#endif
           object[] args2 = ParameterConverter.TransformParameters(caller, worksheet, workbook, args, ref error);
            if (error != null)
                return error;
            ParameterInfo[] methodParameters = Method.GetParameters();
            int max = Math.Min(args2.Length, methodParameters.Length);
            bool needsConversion = false;
            for (int i = 0; i < max; i++)
            {
                object arg = args2[i];
                if (arg is IArray && !(methodParameters[i].ParameterType.IsAssignableFrom(arg.GetType())))
                {
                    needsConversion = true;
                }
            }
            if (needsConversion)
            {
                var accessors = new ArrayAccessor[max];
                int rowCount = -1;
                int columnCount = -1;
                for (int i = 0; i < args2.Length; i++)
                {
                    object arg = args2[i];
                    if (i < methodParameters.Length && arg is IArray &&
                        !(methodParameters[i].ParameterType.IsAssignableFrom(arg.GetType())))
                    {
                        var array = (IArray) arg;
                        if (rowCount != -1)
                        {
                            if (rowCount != array.RowCount)
                                throw new NotSupportedException();
                            if (columnCount != array.ColumnCount)
                                throw new NotSupportedException();
                        }
                        rowCount = array.RowCount;
                        columnCount = array.ColumnCount;
                        accessors[i] = (r, c) => ArrayExpressionHelper.AccessCell( array[r, c]);
                    }
                    else
                    {
                        accessors[i] = (r, c) => arg;
                    }
                }
                return new AddInFunctionArray(accessors, rowCount, columnCount, this, workbook, worksheet, caller);
            }
            return getMethodValue(args2);
        }

        private object getMethodValue(object[] args2)
        {
                object result = Method.Invoke(null, args2);
            if (result is double)
            {
                var d = (double) result;
                if (double.IsNaN(d))
                    return Errors.NaError;
                if (double.IsInfinity(d))
                    return Errors.NumError;
            }
            return result;
        }

        internal override object Invoke(EvaluationContext context, List<Expression> arguments)
        {
            var actualArguments = new object[arguments.Count];
            //\\ParameterInfo[] parameterInfos = Method.GetParameters();
            int parameterIndex = 0;
            ParameterConversionData[] parameterData = ParameterConverter.ParameterData;
            for (int i = 0; i < arguments.Count; i++)
            {
                if (ParameterConverter.ParameterData.Length > parameterIndex)
                {
                    ParameterConversionData pc = parameterData[parameterIndex];
                    if (pc.ParameterType == typeof (Cell))
                    {
                        if (arguments[i] is CellReferenceExpression)
                        {
                            actualArguments[i] = ((CellReferenceExpression) arguments[i]).Cell(context);
                            continue;
                        }
                    }
                }
                actualArguments[i] = arguments[i].GetObject(context);
                if (parameterData.Length > parameterIndex)
                {
                    if (!ParameterConverter.ParameterData[parameterIndex].IsParams)
                    {
                        parameterIndex++;
                    }
                }
            }
            return InvokeOld(context.Target, context.Worksheet, context.Workbook, actualArguments);
        }

        #region Nested type: AddInFunctionArray

        private sealed class AddInFunctionArray : IArray
        {
            private readonly AddInFunction _addInFunction;
            private readonly ArrayAccessor[] _arguments;
            private readonly int _columnCount;
            private readonly int _rowCount;
            private IWorksheet worksheet;
            private ICell caller;
            private IWorkbook workbook;

            public AddInFunctionArray(ArrayAccessor[] arguments, int rowCount, int columnCount,
                                      AddInFunction addInFunction, Workbook workbook, Worksheet worksheet, Cell target)
            {
                Contract.Requires(arguments != null);
                Contract.Requires(addInFunction != null);
                Contract.Requires(rowCount > 0);
                Contract.Requires(columnCount > 0);
                this.worksheet = worksheet;
                this.workbook = workbook;
                this.caller = target;
                _arguments = arguments;
                _rowCount = rowCount;
                _columnCount = columnCount;
                _addInFunction = addInFunction;
            }

            #region IArray Members

            public IEnumerator<object> GetEnumerator()
            {
                if (RowCount > 1 && ColumnCount > 1)
                    throw new ErrorException(Errors.ValueError);
                for (int row = 0; row < _rowCount; row++)
                {
                    for (int column = 0; column < _columnCount; column++)
                    {
                        yield return this[row, column];
                    }
                }
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            public object this[int row, int column]
            {
                get
                {
                    var values = new object[_arguments.Length];
                    for (int i = 0; i < values.Length; i++)
                        values[i] = _arguments[i](row, column);
                    Error error = null;
                    object[] args2 = _addInFunction.ParameterConverter.TransformParameters(this.caller, this.worksheet, this.workbook, values, ref error);
            if (error != null)
                return error;

                    return _addInFunction.getMethodValue(args2);
                }
            }

            public int RowCount
            {
                get { return _rowCount; }
            }

            public int ColumnCount
            {
                get { return _columnCount; }
            }

            #endregion
        }

        #endregion

        #region Nested type: ArrayAccessor

        private delegate object ArrayAccessor(int row, int column);

        #endregion
    }
}